# frozen_string_literal: true
require 'spec_helper'

RSpec.describe 'Git LFS API and storage', feature_category: :source_code_management do
  using RSpec::Parameterized::TableSyntax

  include LfsHttpHelpers
  include ProjectForksHelper
  include WorkhorseHelpers
  include WorkhorseLfsHelpers

  let_it_be(:project, reload: true) { create(:project, :empty_repo) }
  let_it_be(:user) { create(:user) }

  context 'with projects' do
    it_behaves_like 'LFS http requests' do
      let_it_be(:other_project, reload: true) { create(:project, :empty_repo) }

      let(:container) { project }
      let(:authorize_guest) { project.add_guest(user) }
      let(:authorize_download) { project.add_reporter(user) }
      let(:authorize_upload) { project.add_developer(user) }

      context 'project specific LFS settings' do
        let(:body) { upload_body(sample_object) }

        before do
          authorize_upload
          project.update_attribute(:lfs_enabled, project_lfs_enabled)

          subject
        end

        describe 'LFS disabled in project' do
          let(:project_lfs_enabled) { false }

          context 'when uploading' do
            subject(:request) { post_lfs_json(batch_url(project), body, headers) }

            it_behaves_like 'LFS http 404 response'
          end

          context 'when downloading' do
            subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }

            it_behaves_like 'LFS http 404 response'
          end
        end

        describe 'LFS enabled in project' do
          let(:project_lfs_enabled) { true }

          context 'when uploading' do
            subject(:request) { post_lfs_json(batch_url(project), body, headers) }

            it_behaves_like 'LFS http 200 response'
          end

          context 'when downloading' do
            subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }

            it_behaves_like 'LFS http 200 blob response'
          end
        end
      end

      describe 'when fetching LFS object' do
        subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers }

        let(:response) { request && super() }

        before do
          project.lfs_objects << lfs_object
        end

        context 'when LFS uses object storage' do
          before do
            authorize_download
          end

          context 'when proxy download is enabled' do
            before do
              stub_lfs_object_storage(proxy_download: true)
              lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
            end

            it 'responds with the workhorse send-url' do
              expect(response).to have_gitlab_http_status(:ok)
              expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
            end
          end

          context 'when proxy download is disabled' do
            before do
              stub_lfs_object_storage(proxy_download: false)
              lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
            end

            it 'responds with redirect' do
              expect(response).to have_gitlab_http_status(:found)
            end

            it 'responds with the file location' do
              expect(response.location).to include(lfs_object.reload.file.path)
            end
          end
        end

        context 'when deploy key is authorized' do
          let_it_be(:key) { create(:deploy_key) }

          let(:authorization) { authorize_deploy_key }

          before do
            project.deploy_keys << key
          end

          it_behaves_like 'LFS http 200 blob response'
        end

        context 'when using a user key (LFSToken)' do
          let(:authorization) { authorize_user_key }

          context 'when user allowed' do
            before do
              authorize_download
            end

            it_behaves_like 'LFS http 200 blob response'

            context 'when user password is expired' do
              let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user is blocked' do
              let_it_be(:user) { create(:user, :blocked) }

              it_behaves_like 'LFS http 401 response'
            end
          end

          context 'when user not allowed' do
            it_behaves_like 'LFS http 404 response'
          end
        end

        context 'when build is authorized as' do
          let(:authorization) { authorize_ci_project }
          let(:pipeline) { create(:ci_empty_pipeline, project: project) }
          let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

          shared_examples 'can download LFS only from own projects' do
            context 'for owned project' do
              let_it_be(:project) { create(:project, namespace: user.namespace) }

              it_behaves_like 'LFS http 200 blob response'
            end

            context 'for member of project' do
              before do
                authorize_download
              end

              it_behaves_like 'LFS http 200 blob response'
            end

            context 'for other project' do
              let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

              it 'rejects downloading code' do
                expect(response).to have_gitlab_http_status(:not_found)
              end
            end
          end

          context 'administrator', :enable_admin_mode do
            let_it_be(:user) { create(:admin) }

            it_behaves_like 'can download LFS only from own projects'
          end

          context 'regular user' do
            it_behaves_like 'can download LFS only from own projects'
          end

          context 'does not have user' do
            let(:build) { create(:ci_build, :running, pipeline: pipeline) }

            it_behaves_like 'can download LFS only from own projects'
          end
        end
      end

      describe 'when handling LFS batch request' do
        subject(:request) { post_lfs_json batch_url(project), body, headers }

        let(:response) { request && super() }

        before do
          project.lfs_objects << lfs_object
        end

        shared_examples 'process authorization header' do |renew_authorization:|
          let(:response_authorization) do
            authorization_in_action(lfs_actions.first)
          end

          if renew_authorization
            context 'when the authorization comes from a user' do
              it 'returns a new valid LFS token authorization' do
                expect(response_authorization).not_to eq(authorization)
              end

              it 'returns a valid token' do
                username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)

                expect(username).to eq(user.username)
                expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
              end

              it 'generates only one new token per each request' do
                authorizations = lfs_actions.map do |action|
                  authorization_in_action(action)
                end.compact

                expect(authorizations.uniq.count).to eq 1
              end
            end
          else
            context 'when the authorization comes from a token' do
              it 'returns the same authorization header' do
                expect(response_authorization).to eq(authorization)
              end
            end
          end

          def lfs_actions
            json_response['objects'].map { |a| a['actions'] }.compact
          end

          def authorization_in_action(action)
            (action['upload'] || action['download']).dig('header', 'Authorization')
          end
        end

        describe 'download' do
          let(:body) { download_body(sample_object) }

          shared_examples 'an authorized request' do |renew_authorization:|
            context 'when downloading an LFS object that is assigned to our project' do
              it_behaves_like 'LFS http 200 response'

              it 'with href to download' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading an LFS object that is assigned to other project' do
              before do
                lfs_object.update!(projects: [other_project])
              end

              it_behaves_like 'LFS http 200 response'

              it 'with an 404 for specific object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
              end
            end

            context 'when downloading a LFS object that does not exist' do
              let(:body) { download_body(non_existing_object) }

              it_behaves_like 'LFS http 200 response'

              it 'with an 404 for specific object' do
                expect(json_response['objects'].first).to include(non_existing_object)
                expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
              end
            end

            context 'when downloading one existing and one missing LFS object' do
              let(:body) { download_body(multiple_objects) }

              it_behaves_like 'LFS http 200 response'

              it 'responds with download hypermedia link for the existing object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
                expect(json_response['objects'].last).to eq({
                  'oid' => non_existing_object_oid,
                  'size' => non_existing_object_size,
                  'error' => {
                    'code' => 404,
                    'message' => "Object does not exist on the server or you don't have permissions to access it"
                  }
                })
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading two existing LFS objects' do
              let(:body) { download_body(multiple_objects) }
              let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }

              before do
                project.lfs_objects << other_object
              end

              it 'responds with the download hypermedia link for each object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))

                expect(json_response['objects'].last).to include(non_existing_object)
                expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading an LFS object that is stored on object storage' do
              before do
                stub_lfs_object_storage
                lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
              end

              context 'when lfs.object_store.proxy_download=true' do
                before do
                  stub_lfs_object_storage(proxy_download: true)
                end

                it_behaves_like 'LFS http 200 response'

                it 'does return proxied address URL' do
                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
                end
              end

              context 'when "lfs.object_store.proxy_download" is "false"' do
                before do
                  stub_lfs_object_storage(proxy_download: false)
                end

                it_behaves_like 'LFS http 200 response'

                it 'does return direct object storage URL' do
                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/")
                  expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&")
                end

                context 'when feature flag "lfs_batch_direct_downloads" is "false"' do
                  before do
                    stub_feature_flags(lfs_batch_direct_downloads: false)
                  end

                  it_behaves_like 'LFS http 200 response'

                  it 'does return proxied address URL' do
                    expect(json_response['objects'].first).to include(sample_object)
                    expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
                  end
                end
              end
            end

            context 'when sending objects=[]' do
              let(:body) { download_body([]) }

              it_behaves_like 'LFS http expected response code and message' do
                let(:response_code) { 404 }
                let(:message) { 'Not found.' }
              end
            end
          end

          context 'when user is authenticated' do
            before do
              project.add_role(user, role) if role
            end

            it_behaves_like 'an authorized request', renew_authorization: true do
              let(:role) { :reporter }
            end

            context 'when user is not a member of the project' do
              let(:role) { nil }

              it_behaves_like 'LFS http 404 response'
            end

            context 'when user does not have download access' do
              let(:role) { :guest }

              it_behaves_like 'LFS http 404 response'
            end

            context 'when user password is expired' do
              let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }

              let(:role) { :reporter }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user is blocked' do
              let_it_be(:user) { create(:user, :blocked) }

              let(:role) { :reporter }

              it_behaves_like 'LFS http 401 response'
            end
          end

          context 'when using Deploy Tokens' do
            let(:authorization) { authorize_deploy_token }

            context 'when Deploy Token is not valid' do
              let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when Deploy Token is not related to the project' do
              let(:deploy_token) { create(:deploy_token, projects: [other_project]) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when deploy token is from an unrelated group to the project' do
              let(:group) { create(:group) }
              let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when deploy token is from a parent group of the project and valid' do
              let(:group) { create(:group) }
              let(:project) { create(:project, group: group) }
              let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }

              it_behaves_like 'an authorized request', renew_authorization: false
            end

            # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
            context 'when Deploy Token is valid' do
              let(:deploy_token) { create(:deploy_token, projects: [project]) }

              it_behaves_like 'an authorized request', renew_authorization: false
            end
          end

          context 'when build is authorized as' do
            let(:authorization) { authorize_ci_project }

            shared_examples 'can download LFS only from own projects' do |renew_authorization:|
              context 'for own project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: project) }

                before do
                  authorize_download
                end

                it_behaves_like 'an authorized request', renew_authorization: renew_authorization
              end

              context 'for other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                it 'rejects downloading code' do
                  expect(response).to have_gitlab_http_status(:not_found)
                end
              end
            end

            context 'administrator', :enable_admin_mode do
              let_it_be(:user) { create(:admin) }

              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: true
            end

            context 'regular user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: true
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: false
            end
          end

          context 'when user is not authenticated' do
            let(:authorization) { nil }

            describe 'is accessing public project' do
              let_it_be(:project) { create(:project, :public) }

              it_behaves_like 'LFS http 200 response'

              it 'returns href to download' do
                expect(json_response).to eq({
                  'objects' => [
                    {
                      'oid' => sample_oid,
                      'size' => sample_size,
                      'authenticated' => true,
                      'actions' => {
                        'download' => {
                          'href' => objects_url(project, sample_oid),
                          'header' => {}
                        }
                      }
                    }
                  ]
                })
              end
            end

            describe 'is accessing non-public project' do
              it_behaves_like 'LFS http 401 response'
            end
          end
        end

        describe 'upload' do
          let_it_be(:project) { create(:project, :public) }

          let(:body) { upload_body(sample_object) }

          shared_examples 'pushes new LFS objects' do |renew_authorization:|
            let(:sample_size) { 150.megabytes }
            let(:sample_oid) { non_existing_object_oid }

            it_behaves_like 'LFS http 200 response'

            it 'responds with upload hypermedia link' do
              expect(json_response['objects']).to be_kind_of(Array)
              expect(json_response['objects'].first).to include(sample_object)
              expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))

              headers = json_response['objects'].first['actions']['upload']['header']
              expect(headers['Content-Type']).to eq('application/octet-stream')
              expect(headers['Transfer-Encoding']).to eq('chunked')
            end

            it_behaves_like 'process authorization header', renew_authorization: renew_authorization
          end

          describe 'when request is authenticated' do
            describe 'when user has project push access' do
              before do
                authorize_upload
              end

              context 'when pushing an LFS object that already exists' do
                shared_examples_for 'batch upload with existing LFS object' do
                  it_behaves_like 'LFS http 200 response'

                  it 'responds with links to the object in the project' do
                    expect(json_response['objects']).to be_kind_of(Array)
                    expect(json_response['objects'].first).to include(sample_object)
                    expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
                    expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
                    expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))

                    headers = json_response['objects'].first['actions']['upload']['header']
                    expect(headers['Content-Type']).to eq('application/octet-stream')
                    expect(headers['Transfer-Encoding']).to eq('chunked')
                  end

                  it_behaves_like 'process authorization header', renew_authorization: true
                end

                context 'in another project' do
                  before do
                    lfs_object.update!(projects: [other_project])
                  end

                  it_behaves_like 'batch upload with existing LFS object'
                end

                context 'in source of fork project' do
                  let(:other_project) { create(:project, :empty_repo) }
                  let(:project) { fork_project(other_project) }

                  before do
                    lfs_object.update!(projects: [other_project])
                  end

                  context 'when user has access to both the parent and fork' do
                    before do
                      project.add_developer(user)
                      other_project.add_developer(user)
                    end

                    it 'links existing LFS objects to other project' do
                      expect(Gitlab::AppJsonLogger).to receive(:info).with(
                        message: "LFS object auto-linked to forked project",
                        lfs_object_oid: lfs_object.oid,
                        lfs_object_size: lfs_object.size,
                        source_project_id: other_project.id,
                        source_project_path: other_project.full_path,
                        target_project_id: project.id,
                        target_project_path: project.full_path).and_call_original
                      expect(json_response['objects']).to be_kind_of(Array)
                      expect(json_response['objects'].first).to include(sample_object)
                      expect(json_response['objects'].first).not_to have_key('actions')

                      expect(lfs_object.reload.projects.pluck(:id)).to match_array([other_project.id, project.id])
                    end
                  end

                  context 'when user does not have access to parent' do
                    before do
                      project.add_developer(user)
                    end

                    it_behaves_like 'batch upload with existing LFS object'
                  end
                end
              end

              context 'when pushing a LFS object that does not exist' do
                it_behaves_like 'pushes new LFS objects', renew_authorization: true
              end

              context 'when pushing one new and one existing LFS object' do
                let(:body) { upload_body(multiple_objects) }

                it_behaves_like 'LFS http 200 response'

                it 'responds with upload hypermedia link for the new object' do
                  expect(json_response['objects']).to be_kind_of(Array)

                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first).not_to have_key('actions')

                  expect(json_response['objects'].last).to include(non_existing_object)
                  expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))

                  headers = json_response['objects'].last['actions']['upload']['header']
                  expect(headers['Content-Type']).to eq('application/octet-stream')
                  expect(headers['Transfer-Encoding']).to eq('chunked')
                end

                it_behaves_like 'process authorization header', renew_authorization: true
              end
            end

            context 'when user does not have push access' do
              it_behaves_like 'LFS http 403 response'
            end

            context 'when build is authorized' do
              let(:authorization) { authorize_ci_project }
              let(:pipeline) { create(:ci_empty_pipeline, project: project) }

              context 'build has an user' do
                let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

                context 'tries to push to own project' do
                  it_behaves_like 'LFS http 403 response'
                end

                context 'tries to push to other project' do
                  let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                  # I'm not sure what this tests that is different from the previous test
                  it_behaves_like 'LFS http 403 response'
                end
              end

              context 'does not have user' do
                let(:build) { create(:ci_build, :running, pipeline: pipeline) }

                it_behaves_like 'LFS http 403 response'
              end
            end

            context 'when deploy key has project push access' do
              let(:key) { create(:deploy_key) }
              let(:authorization) { authorize_deploy_key }

              before do
                project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
              end

              it_behaves_like 'pushes new LFS objects', renew_authorization: false
            end
          end

          context 'when user is not authenticated' do
            let(:authorization) { nil }

            context 'when user has push access' do
              before do
                authorize_upload
              end

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user does not have push access' do
              it_behaves_like 'LFS http 401 response'
            end
          end
        end

        describe 'unsupported' do
          let(:body) { request_body('other', sample_object) }

          it_behaves_like 'LFS http 404 response'
        end
      end

      describe 'when handling LFS batch request on a read-only GitLab instance' do
        subject { post_lfs_json(batch_url(project), body, headers) }

        before do
          allow(Gitlab::Database).to receive(:read_only?) { true }

          project.add_maintainer(user)

          subject
        end

        context 'when downloading' do
          let(:body) { download_body(sample_object) }

          it_behaves_like 'LFS http 200 response'
        end

        context 'when uploading' do
          let(:body) { upload_body(sample_object) }

          it_behaves_like 'LFS http expected response code and message' do
            let(:response_code) { 403 }
            let(:message) { 'You cannot write to this read-only GitLab instance.' }
          end
        end
      end

      describe 'when pushing a LFS object' do
        let(:include_workhorse_jwt_header) { true }

        shared_examples 'unauthorized' do
          context 'and request is sent by gitlab-workhorse to authorize the request' do
            before do
              put_authorize
            end

            it_behaves_like 'LFS http 401 response'
          end

          context 'and request is sent by gitlab-workhorse to finalize the upload' do
            before do
              put_finalize
            end

            it_behaves_like 'LFS http 401 response'
          end

          context 'and request is sent with a malformed headers' do
            before do
              put_finalize('/etc/passwd')
            end

            it_behaves_like 'LFS http 401 response'
          end
        end

        shared_examples 'forbidden' do
          context 'and request is sent by gitlab-workhorse to authorize the request' do
            before do
              put_authorize
            end

            it_behaves_like 'LFS http 403 response'
          end

          context 'and request is sent by gitlab-workhorse to finalize the upload' do
            before do
              put_finalize
            end

            it_behaves_like 'LFS http 403 response'
          end

          context 'and request is sent with a malformed headers' do
            before do
              put_finalize('/etc/passwd')
            end

            it_behaves_like 'LFS http 403 response'
          end
        end

        describe 'to one project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
          describe 'when user is authenticated' do
            describe 'when user has push access to the project' do
              before do
                project.add_developer(user)
              end

              context 'and the request bypassed workhorse' do
                it 'raises an exception' do
                  expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
                end
              end

              context 'and request is sent by gitlab-workhorse to authorize the request' do
                shared_examples 'a valid response' do
                  before do
                    put_authorize
                  end

                  it_behaves_like 'LFS http 200 workhorse response'
                end

                shared_examples 'a local file' do
                  it_behaves_like 'a valid response' do
                    it 'responds with status 200, location of LFS store and object details' do
                      expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
                      expect(json_response['RemoteObject']).to be_nil
                      expect(json_response['LfsOid']).to eq(sample_oid)
                      expect(json_response['LfsSize']).to eq(sample_size)
                    end
                  end
                end

                context 'when using local storage' do
                  it_behaves_like 'a local file'
                end

                context 'when using remote storage' do
                  context 'when direct upload is enabled' do
                    before do
                      stub_lfs_object_storage(enabled: true, direct_upload: true)
                    end

                    it_behaves_like 'a valid response' do
                      it 'responds with status 200, location of LFS remote store and object details' do
                        expect(json_response).not_to have_key('TempPath')
                        expect(json_response['RemoteObject']).to have_key('ID')
                        expect(json_response['RemoteObject']).to have_key('GetURL')
                        expect(json_response['RemoteObject']).to have_key('StoreURL')
                        expect(json_response['RemoteObject']).to have_key('DeleteURL')
                        expect(json_response['RemoteObject']).to have_key('MultipartUpload')
                        expect(json_response['LfsOid']).to eq(sample_oid)
                        expect(json_response['LfsSize']).to eq(sample_size)
                      end
                    end
                  end

                  context 'when direct upload is disabled' do
                    before do
                      stub_lfs_object_storage(enabled: true, direct_upload: false)
                    end

                    it_behaves_like 'a local file'
                  end
                end
              end

              context 'and request is sent by gitlab-workhorse to finalize the upload' do
                before do
                  put_finalize
                end

                it_behaves_like 'LFS http 200 response'

                it 'LFS object is linked to the project' do
                  expect(lfs_object.projects.pluck(:id)).to include(project.id)
                end
              end

              context 'and request to finalize the upload is not sent by gitlab-workhorse' do
                it 'fails with a JWT decode error' do
                  expect { put_finalize(verified: false) }.to raise_error(JWT::DecodeError)
                end
              end

              context 'and the uploaded file is invalid' do
                where(:size, :sha256, :status) do
                  nil | nil | :ok # Test setup sanity check
                  0 | nil | :bad_request
                  nil | 'a' * 64 | :bad_request
                end

                with_them do
                  it 'validates the upload size and SHA256' do
                    put_finalize(size: size, sha256: sha256)

                    expect(response).to have_gitlab_http_status(status)
                  end
                end
              end

              context 'and workhorse requests upload finalize for a new LFS object' do
                before do
                  lfs_object.destroy!
                end

                context 'with object storage enabled' do
                  context 'and direct upload enabled' do
                    let!(:fog_connection) do
                      stub_lfs_object_storage(direct_upload: true)
                    end

                    let(:tmp_object) do
                      fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang
                        key: 'tmp/uploads/12312300',
                        body: 'x' * sample_size
                      )
                    end

                    ['123123', '../../123123'].each do |remote_id|
                      context "with invalid remote_id: #{remote_id}" do
                        subject do
                          put_finalize(remote_object: tmp_object, args: {
                            'file.remote_id' => remote_id
                          })
                        end

                        it 'responds with status 403' do
                          subject

                          expect(response).to have_gitlab_http_status(:forbidden)
                        end
                      end
                    end

                    context 'with valid remote_id' do
                      subject do
                        put_finalize(remote_object: tmp_object, args: {
                          'file.remote_id' => '12312300',
                          'file.name' => 'name'
                        })
                      end

                      it 'responds with status 200' do
                        subject

                        expect(response).to have_gitlab_http_status(:ok)

                        object = LfsObject.find_by_oid(sample_oid)
                        expect(object).to be_present
                        expect(object.file.read).to eq(tmp_object.body)
                      end

                      it 'schedules migration of file to object storage' do
                        subject

                        expect(LfsObject.last.projects).to include(project)
                      end

                      it 'have valid file' do
                        subject

                        expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
                        expect(LfsObject.last.file).to be_exists
                      end
                    end
                  end
                end
              end

              context 'without the lfs object' do
                before do
                  lfs_object.destroy!
                end

                it 'rejects slashes in the tempfile name (path traversal)' do
                  put_finalize('../bar', with_tempfile: true)
                  expect(response).to have_gitlab_http_status(:bad_request)
                end

                context 'not sending the workhorse jwt header' do
                  let(:include_workhorse_jwt_header) { false }

                  it 'rejects the request' do
                    put_finalize(with_tempfile: true)

                    expect(response).to have_gitlab_http_status(:unprocessable_entity)
                  end
                end
              end
            end

            describe 'and user does not have push access' do
              before do
                project.add_reporter(user)
              end

              it_behaves_like 'forbidden'
            end
          end

          context 'when build is authorized' do
            let(:authorization) { authorize_ci_project }
            let(:pipeline) { create(:ci_empty_pipeline, project: project) }

            context 'build has an user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              context 'tries to push to own project' do
                before do
                  project.add_developer(user)
                  put_authorize
                end

                it_behaves_like 'LFS http 403 response'
              end

              context 'tries to push to other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                before do
                  put_authorize
                end

                it_behaves_like 'LFS http 404 response'
              end
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              before do
                put_authorize
              end

              it_behaves_like 'LFS http 403 response'
            end
          end

          describe 'when using a user key (LFSToken)' do
            let(:authorization) { authorize_user_key }

            context 'when user allowed' do
              before do
                project.add_developer(user)
                put_authorize
              end

              it_behaves_like 'LFS http 200 workhorse response'

              context 'when user password is expired' do
                let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }

                it_behaves_like 'LFS http 401 response'
              end

              context 'when user is blocked' do
                let_it_be(:user) { create(:user, :blocked) }

                it_behaves_like 'LFS http 401 response'
              end
            end

            context 'when user not allowed' do
              before do
                put_authorize
              end

              it_behaves_like 'LFS http 404 response'
            end
          end

          context 'for unauthenticated' do
            let(:authorization) { nil }

            it_behaves_like 'unauthorized'
          end
        end

        describe 'to a forked project' do
          let_it_be_with_reload(:upstream_project) { create(:project, :public) }
          let_it_be(:project_owner) { create(:user) }

          let(:project) { fork_project(upstream_project, project_owner) }

          describe 'when user is authenticated' do
            describe 'when user has push access to the project' do
              before do
                project.add_developer(user)
              end

              context 'and request is sent by gitlab-workhorse to authorize the request' do
                before do
                  put_authorize
                end

                it_behaves_like 'LFS http 200 workhorse response'

                it 'with location of LFS store and object details' do
                  expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
                  expect(json_response['LfsOid']).to eq(sample_oid)
                  expect(json_response['LfsSize']).to eq(sample_size)
                end
              end

              context 'and request is sent by gitlab-workhorse to finalize the upload' do
                before do
                  put_finalize
                end

                it_behaves_like 'LFS http 200 response'

                it 'LFS object is linked to the forked project' do
                  expect(lfs_object.projects.pluck(:id)).to include(project.id)
                end
              end
            end

            describe 'when user has push access to upstream project' do
              before do
                upstream_project.add_maintainer(user)
              end

              context 'an MR exists on target forked project' do
                let(:allow_collaboration) { true }
                let(:merge_request) do
                  create(:merge_request,
                         target_project: upstream_project,
                         source_project: project,
                         allow_collaboration: allow_collaboration)
                end

                before do
                  merge_request
                end

                context 'with allow_collaboration option set to true' do
                  context 'and request is sent by gitlab-workhorse to authorize the request' do
                    before do
                      put_authorize
                    end

                    it_behaves_like 'LFS http 200 workhorse response'
                  end

                  context 'and request is sent by gitlab-workhorse to finalize the upload' do
                    before do
                      put_finalize
                    end

                    it_behaves_like 'LFS http 200 response'
                  end
                end

                context 'with allow_collaboration option set to false' do
                  context 'request is sent by gitlab-workhorse to authorize the request' do
                    let(:allow_collaboration) { false }

                    before do
                      put_authorize
                    end

                    it_behaves_like 'forbidden'
                  end
                end
              end
            end

            describe 'and user does not have push access' do
              it_behaves_like 'forbidden'
            end
          end

          context 'when build is authorized' do
            let(:authorization) { authorize_ci_project }
            let(:pipeline) { create(:ci_empty_pipeline, project: project) }

            before do
              put_authorize
            end

            context 'build has an user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              context 'tries to push to own project' do
                it_behaves_like 'LFS http 403 response'
              end

              context 'tries to push to other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                # I'm not sure what this tests that is different from the previous test
                it_behaves_like 'LFS http 403 response'
              end
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              it_behaves_like 'LFS http 403 response'
            end
          end

          context 'for unauthenticated' do
            let(:authorization) { nil }

            it_behaves_like 'unauthorized'
          end

          describe 'and second project not related to fork or a source project' do
            let_it_be(:second_project) { create(:project) }

            before do
              second_project.add_maintainer(user)
              upstream_project.lfs_objects << lfs_object
            end

            context 'when pushing the same LFS object to the second project' do
              before do
                put_finalize(with_tempfile: true, to_project: second_project)
              end

              it_behaves_like 'LFS http 200 response'

              it 'links the LFS object to the project' do
                expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
              end
            end
          end
        end

        def put_authorize(verified: true)
          authorize_headers = headers
          authorize_headers.merge!(workhorse_internal_api_request_header) if verified

          put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
        end
      end
    end
  end

  context 'with project wikis' do
    it_behaves_like 'LFS http requests' do
      let(:container) { create(:project_wiki, :empty_repo, project: project) }
      let(:authorize_guest) { project.add_guest(user) }
      let(:authorize_download) { project.add_reporter(user) }
      let(:authorize_upload) { project.add_developer(user) }
    end
  end

  context 'with snippets' do
    # LFS is not supported on snippets, so we override the shared examples
    # to expect 404 responses instead.
    [
      'LFS http 200 response',
      'LFS http 200 blob response',
      'LFS http 403 response'
    ].each do |examples|
      shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' }
    end

    context 'with project snippets' do
      it_behaves_like 'LFS http requests' do
        let(:container) { create(:project_snippet, :empty_repo, project: project) }
        let(:authorize_guest) { project.add_guest(user) }
        let(:authorize_download) { project.add_reporter(user) }
        let(:authorize_upload) { project.add_developer(user) }
      end
    end

    context 'with personal snippets' do
      it_behaves_like 'LFS http requests' do
        let(:container) { create(:personal_snippet, :empty_repo) }
        let(:authorize_upload) { container.update!(author: user) }
      end
    end
  end
end
